﻿namespace DCHealthCheck {

    #region Usings
    using DCHealthCheck.Common;
    using DCHealthCheck.DomainModel;
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Net;
    using System.Text;
    using System.Threading; 
    #endregion

    /// <summary>
    /// Structured display of the output of the 'w32tm /query /status /verbose' command
    /// </summary>
    /// <remarks>Requires Windows 2008 or later.</remarks>
    public class TimeSyncInfoBuilder {

        #region Members
        private string CommandFilename {
            get {
                return "w32tm.exe";
            }
        }

        private string CommandArguments {
            get {
                return string.Format(" /query /computer:{0} /status /verbose", this.TimeSyncInfo.Source);
            }
        }

        public string ErrorMessage { get; private set; }

        private static log4net.ILog Log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private readonly NTPClient NtpClient;

        private static readonly long OneSecondTicks = 10000000;

        private TimeSyncInfo TimeSyncInfo;

        private List<string> W32tmOutput { get; set; }
        #endregion

        #region Constructors
        private TimeSyncInfoBuilder() {
        }

        public TimeSyncInfoBuilder(TimeSyncInfo timeSyncInfo, NTPClient ntpClient)
            : this() {
            if (timeSyncInfo == null) {
                throw new ArgumentNullException("timeSyncInfo");
            }
            if (ntpClient == null) {
                throw new ArgumentNullException("ntpClient");
            }

            this.TimeSyncInfo = timeSyncInfo;
            this.NtpClient = ntpClient;
        }

        public TimeSyncInfoBuilder(TimeSyncInfo timeSyncInfo, string sourceHostName)
            : this() {
            if (timeSyncInfo == null) {
                throw new ApplicationException("timeSyncInfo cannot be null.");
            }
            if (sourceHostName == null) {
                throw new ApplicationException("hostName cannot be null.");
            }
            this.TimeSyncInfo = timeSyncInfo;
            this.TimeSyncInfo.Source = sourceHostName;
        } 
        #endregion

        #region Methods
        public TimeSyncInfo GetTimeSyncInfo() {
            if (this.NtpClient != null) {
                this.TimeSyncInfo.LeapIndicator = NtpClient.LeapIndicator.ToString();
                this.TimeSyncInfo.Stratum = NtpClient.Stratum.ToString();
                this.TimeSyncInfo.Precision = NtpClient.Precision.ToString();
                this.TimeSyncInfo.RootDelay = NtpClient.RootDelay.ToString();
                this.TimeSyncInfo.RootDispersion = NtpClient.RootDispersion.ToString();
                this.TimeSyncInfo.Source = NtpClient.Source;
                this.TimeSyncInfo.SourceIPAddress = NtpClient.SourceIPAddress.ToString();
                this.TimeSyncInfo.ServerRole = NtpClient.ServerMode.ToString();
                this.TimeSyncInfo.PollInterval = NtpClient.PollInterval.ToString();
                this.TimeSyncInfo.PhaseOffset = NtpClient.LocalClockOffset.ToString();
            }
            else {
                // process w32tm command
                try {
                    this.W32tmOutput = Utility.ExecuteShellCommand(this.CommandFilename, this.CommandArguments);

                    bool validResponse = (this.W32tmOutput != null) && (this.W32tmOutput.Count > 0) &&
                        (this.W32tmOutput.Any(x => x.Contains("Leap Indicator")) ||
                        this.W32tmOutput.Any(x => x.Contains("Stratum")));

                    if (validResponse) {
                        #region Leap Indicator: 0(no warning)
                        string leapIndicatorTag = "Leap Indicator:";
                        string leapIndicatorLine = this.W32tmOutput.Where(x => x.Contains(leapIndicatorTag)).FirstOrDefault();
                        if ((leapIndicatorLine != null) && leapIndicatorLine.Contains("(") && leapIndicatorLine.Contains(")")) {
                            leapIndicatorLine = leapIndicatorLine.Replace(leapIndicatorTag, string.Empty);
                            leapIndicatorLine = leapIndicatorLine.Substring(leapIndicatorLine.IndexOf("(") + 1);
                            this.TimeSyncInfo.LeapIndicator = leapIndicatorLine.Substring(0, leapIndicatorLine.LastIndexOf(")")).Trim();
                        }
                        #endregion

                        #region Stratum: 3 (secondary reference - syncd by (S)NTP)
                        string stratumTag = "Stratum:";
                        string stratumLine = this.W32tmOutput.Where(x => x.Contains(stratumTag)).FirstOrDefault();
                        if ((stratumLine != null) && stratumLine.Contains("(") && stratumLine.Contains(")")) {
                            stratumLine = stratumLine.Replace(stratumTag, string.Empty);
                            stratumLine = stratumLine.Substring(stratumLine.IndexOf("(") + 1);
                            this.TimeSyncInfo.Stratum = stratumLine.Substring(0, stratumLine.LastIndexOf(")")).Trim();
                            if (this.TimeSyncInfo.Stratum.StartsWith("secondary", StringComparison.OrdinalIgnoreCase)) {
                                this.TimeSyncInfo.Stratum = "SecondaryReference";
                            }
                        }
                        #endregion

                        #region Precision: -6 (15.625ms per tick)
                        string precisionTag = "Precision:";
                        string precisionLine = this.W32tmOutput.Where(x => x.Contains(precisionTag)).FirstOrDefault();
                        if ((precisionLine != null) && precisionLine.Contains("(") && precisionLine.Contains(")")) {
                            precisionLine = precisionLine.Replace(precisionTag, string.Empty);
                            precisionLine = precisionLine.Substring(precisionLine.IndexOf("(") + 1);
                            precisionLine = precisionLine.Substring(0, precisionLine.LastIndexOf(")")).Replace("ms per tick", string.Empty).Trim();
                            double precisionMs = 0;
                            if (Double.TryParse(precisionLine, out precisionMs)) {
                                this.TimeSyncInfo.Precision = TimeSpan.FromMilliseconds(precisionMs).ToString();
                            }
                        }
                        #endregion

                        #region Root Delay: 0.0625000s
                        string rootDelayTag = "Root Delay:";
                        string rootDelayLine = this.W32tmOutput.Where(x => x.Contains(rootDelayTag)).FirstOrDefault();
                        if (rootDelayLine != null) {
                            rootDelayLine = rootDelayLine.Replace(rootDelayTag, string.Empty).Replace("s", string.Empty).Trim();
                            double rootDelaySeconds = 0;
                            if (Double.TryParse(rootDelayLine, out rootDelaySeconds)) {
                                long rootDelayTicks = (long)(rootDelaySeconds * (double)OneSecondTicks);
                                this.TimeSyncInfo.RootDelay = TimeSpan.FromTicks(rootDelayTicks).ToString();
                            }
                        }
                        #endregion

                        #region Root Dispersion: 0.0975521s
                        string rootDispersionTag = "Root Dispersion:";
                        string rootDispersionLine = this.W32tmOutput.Where(x => x.Contains(rootDispersionTag)).FirstOrDefault();
                        if (rootDispersionLine != null) {
                            rootDispersionLine = rootDispersionLine.Replace(rootDispersionTag, string.Empty).Replace("s", string.Empty).Trim();
                            double rootDispersionSeconds = 0;
                            if (Double.TryParse(rootDispersionLine, out rootDispersionSeconds)) {
                                long rootDispersionTicks = (long)(rootDispersionSeconds * (double)OneSecondTicks);
                                this.TimeSyncInfo.RootDispersion = TimeSpan.FromTicks(rootDispersionTicks).ToString();
                            }
                        }
                        #endregion

                        #region ReferenceId: 0xABCDEFAB (source IP:  n.n.n.n)
                        string ipAddressTag = "source IP:";
                        string ipAddressLine = this.W32tmOutput.Where(x => x.Contains(ipAddressTag)).FirstOrDefault();
                        if (ipAddressLine != null) {
                            ipAddressLine = ipAddressLine
                                .Substring(ipAddressLine.IndexOf(ipAddressTag, StringComparison.OrdinalIgnoreCase))
                                .Replace(ipAddressTag, string.Empty).Replace(")", string.Empty)
                                .Trim();
                            IPAddress ipAddress = null;
                            if (IPAddress.TryParse(ipAddressLine, out ipAddress)) {
                                this.TimeSyncInfo.SourceIPAddress = ipAddress.ToString();
                            }
                        }
                        #endregion

                        #region Source: dcname.domain.company.com
                        string sourceTag = "Source:";
                        string sourceLine = this.W32tmOutput.Where(x => x.Contains(sourceTag)).FirstOrDefault();
                        if (sourceLine != null) {
                            this.TimeSyncInfo.Source = sourceLine.Replace(sourceTag, string.Empty).Trim();
                        }
                        #endregion

                        #region Last Successful Sync Time: 8/30/2012 1:23:23 PM
                        string lastSyncTag = "Last Successful Sync Time:";
                        string lastSyncLine = this.W32tmOutput.Where(x => x.Contains(lastSyncTag)).FirstOrDefault();
                        if (lastSyncLine != null) {
                            lastSyncLine = lastSyncLine.Replace(lastSyncTag, string.Empty).Trim();
                            DateTime lastSuccessfulSync = DateTime.MinValue;
                            if (DateTime.TryParse(lastSyncLine, CultureInfo.CurrentCulture.DateTimeFormat, DateTimeStyles.None, out lastSuccessfulSync)) {
                                this.TimeSyncInfo.LastSuccessfulSync = lastSuccessfulSync;
                            }
                        }
                        #endregion

                        #region Poll Interval: 10 (1024s)
                        string pollIntervalTag = "Poll Interval:";
                        string pollIntervalLine = this.W32tmOutput.Where(x => x.Contains(pollIntervalTag)).FirstOrDefault();
                        if (pollIntervalLine != null) {
                            pollIntervalLine = pollIntervalLine.Replace(pollIntervalTag, string.Empty).Trim();
                            pollIntervalLine = pollIntervalLine.Substring(pollIntervalLine.IndexOf("(") + 1);
                            pollIntervalLine = pollIntervalLine.Substring(0, pollIntervalLine.LastIndexOf(")")).Replace("s", string.Empty).Trim();
                            double pollIntervalSeconds = 0;
                            if (Double.TryParse(pollIntervalLine, out pollIntervalSeconds)) {
                                this.TimeSyncInfo.PollInterval = TimeSpan.FromSeconds(pollIntervalSeconds).ToString();
                            }
                        }
                        #endregion

                        #region Phase Offset: 0.0000693s
                        string phaseOffsetTag = "Phase Offset:";
                        string phaseOffsetLine = this.W32tmOutput.Where(x => x.Contains(phaseOffsetTag)).FirstOrDefault();
                        if (phaseOffsetLine != null) {
                            phaseOffsetLine = phaseOffsetLine.Replace(phaseOffsetTag, string.Empty).Replace("s", string.Empty).Trim();
                            double phaseOffsetSeconds = 0;
                            if (Double.TryParse(phaseOffsetLine, out phaseOffsetSeconds)) {
                                long phaseOffsetTicks = (long)(phaseOffsetSeconds * (double)OneSecondTicks);
                                this.TimeSyncInfo.PhaseOffset = TimeSpan.FromTicks(phaseOffsetTicks).ToString();
                            }
                        }
                        #endregion

                        #region ClockRate: 0.0156003s
                        string clockRateTag = "ClockRate:";
                        string clockRateLine = this.W32tmOutput.Where(x => x.Contains(clockRateTag)).FirstOrDefault();
                        if (clockRateLine != null) {
                            clockRateLine = clockRateLine.Replace(clockRateTag, string.Empty).Replace("s", string.Empty).Trim();
                            double clockRateSeconds = 0;
                            if (Double.TryParse(clockRateLine, out clockRateSeconds)) {
                                long clockRateTicks = (long)(clockRateSeconds * (double)OneSecondTicks);
                                this.TimeSyncInfo.ClockRate = TimeSpan.FromTicks(clockRateTicks).ToString();
                            }
                        }
                        #endregion

                        #region State Machine: 2 (Sync)
                        string stateMachineTag = "State Machine:";
                        string stateMachineLine = this.W32tmOutput.Where(x => x.Contains(stateMachineTag)).FirstOrDefault();
                        if ((stateMachineLine != null) && stateMachineLine.Contains("(") && stateMachineLine.Contains(")")) {
                            stateMachineLine = stateMachineLine.Replace(stateMachineTag, string.Empty).Trim();
                            stateMachineLine = stateMachineLine.Substring(stateMachineLine.IndexOf("(") + 1);
                            this.TimeSyncInfo.StateMachine = stateMachineLine.Substring(0, stateMachineLine.LastIndexOf(")")).Trim();
                        }
                        #endregion

                        #region Time Source Flags: 2 (Authenticated )
                        string timeSourceFlagsTag = "Time Source Flags:";
                        string timeSourceFlagsLine = this.W32tmOutput.Where(x => x.Contains(timeSourceFlagsTag)).FirstOrDefault();
                        if ((timeSourceFlagsLine != null) && timeSourceFlagsLine.Contains("(") && timeSourceFlagsLine.Contains(")")) {
                            timeSourceFlagsLine = timeSourceFlagsLine.Replace(timeSourceFlagsTag, string.Empty).Trim();
                            timeSourceFlagsLine = timeSourceFlagsLine.Substring(timeSourceFlagsLine.IndexOf("(") + 1);
                            this.TimeSyncInfo.TimeSourceFlags = timeSourceFlagsLine.Substring(0, timeSourceFlagsLine.LastIndexOf(")")).Trim();
                        }
                        #endregion

                        #region Server Role: 64 (Time Service)
                        string serverRoleFlagsTag = "Server Role:";
                        string serverRoleFlagsLine = this.W32tmOutput.Where(x => x.Contains(serverRoleFlagsTag)).FirstOrDefault();
                        if ((serverRoleFlagsLine != null) && serverRoleFlagsLine.Contains("(") && serverRoleFlagsLine.Contains(")")) {
                            serverRoleFlagsLine = serverRoleFlagsLine.Replace(serverRoleFlagsTag, string.Empty).Trim();
                            serverRoleFlagsLine = serverRoleFlagsLine.Substring(serverRoleFlagsLine.IndexOf("(") + 1);
                            this.TimeSyncInfo.ServerRole = serverRoleFlagsLine.Substring(0, serverRoleFlagsLine.LastIndexOf(")")).Trim();
                        }
                        #endregion

                        #region Last Sync Error: 0 (The command completed successfully.)
                        string lastSyncErrorTag = "Last Sync Error:";
                        string lastSyncErrorLine = this.W32tmOutput.Where(x => x.Contains(lastSyncErrorTag)).FirstOrDefault();
                        if ((lastSyncErrorLine != null) && lastSyncErrorLine.Contains("(") && lastSyncErrorLine.Contains(")")) {
                            lastSyncErrorLine = lastSyncErrorLine.Replace(lastSyncErrorTag, string.Empty).Trim();
                            lastSyncErrorLine = lastSyncErrorLine.Substring(lastSyncErrorLine.IndexOf("(") + 1);
                            this.TimeSyncInfo.LastSyncError = lastSyncErrorLine.Substring(0, lastSyncErrorLine.LastIndexOf(")")).Trim();
                        }
                        #endregion

                        #region Time since Last Good Sync Time: 752.9386772s
                        string timeSinceTag = "Time since Last Good Sync Time:";
                        string timeSinceLine = this.W32tmOutput.Where(x => x.Contains(timeSinceTag)).FirstOrDefault();
                        if (timeSinceLine != null) {
                            timeSinceLine = timeSinceLine.Replace(timeSinceTag, string.Empty).Replace("s", string.Empty).Trim();
                            double timeSinceValue = 0;
                            if (Double.TryParse(timeSinceLine, out timeSinceValue)) {
                                this.TimeSyncInfo.TimeSinceLastGoodSync = TimeSpan.FromSeconds(timeSinceValue).ToString();
                            }
                        }
                        #endregion
                    } // if (validResponse)
                }
                catch (Exception e) {
                    Log.WarnFormat("[ThreadID: {0}] {1} Host: {2} Exception: {3}",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), this.TimeSyncInfo.Source, e.VerboseExceptionString());

                    if (e.Message.Contains("The following error occurred: The service has not been started. (0x80070426)")) {
                        this.ErrorMessage = e.Message;
                    }
                }
            }

            return this.TimeSyncInfo;
        }

        public override string ToString() {
            if ((this.W32tmOutput != null) && (this.W32tmOutput.Count > 0)) {
                return this.W32tmOutput.ToFormattedString();
            }
            else if (this.NtpClient != null) {
                return this.NtpClient.ToString();
            }
            else {
                return string.Empty;
            }
        } 
        #endregion
    }
}
